<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\iocc;

#use \ReflectionClass;

class ObjectBuilder
{
	/**
	 * @var array<string,bool>
	 */
	private $objectsBeingBuilt = array();
	
	/**
	 * @var bool
	 */
	private $is53;
	
	public function __construct()
	{
		$this->is53 = version_compare(PHP_VERSION, '5.3', '>=');
	}
	
	/**
	 * @param  ObjectDefinition $definition
	 * @param  Container $container
	 * @param  array<int,mixed>? $resolveArgs
	 * @return object
	 * @throws ObjectBuildingException
	 */
	public function buildObject(ObjectDefinition $definition, Container $container, array $resolveArgs = null)
	{
		$className  = $definition->getClass();
		$objectHash = spl_object_hash($definition);
		
		if (isset($this->objectsBeingBuilt[$objectHash]))
		{
			throw new ObjectBuildingException(sprintf('Circular dependency for type [%s].', $className));
		}
		
		$this->objectsBeingBuilt[$objectHash] = true;
		
		try
		{
			$class = new ReflectionClass($className);
			
			$object = $this->instantiate(
				$class, $this->resolveValues($definition->getArguments($resolveArgs), $container)
			);
			
			$this->injectFields($class, $object, $definition->getFields(), $container);
			
			$this->injectMethods($object, $definition->getMethodCalls(), $container);
			
			unset($this->objectsBeingBuilt[$objectHash]);
			
			return $object;
		}
		catch (ObjectBuildingException $x)
		{
			unset($this->objectsBeingBuilt[$objectHash]);
			
			throw $x;
		}
		catch (Exception $x)
		{
			unset($this->objectsBeingBuilt[$objectHash]);
			
			throw new ObjectBuildingException(
				sprintf('Exception occured while building an object of type [%s].', $className), null, $x
			);
		}
	}
	
	/**
	 * @param  ReflectionClass $class
	 * @param  array<int,mixed> $arguments
	 */
	private function instantiate(ReflectionClass $class, array $arguments)
	{
		if (!$class->isInstantiable())
		{
			throw new ObjectBuildingException(sprintf('[%s] is not instantiable.', $class->getName()));
		}
		
		return empty($arguments) ? $class->newInstance() : $class->newInstanceArgs($arguments);
	}
	
	/**
	 * @param  ReflectionClass $class
	 * @param  object $object
	 * @param  array<string,mixed> $fields
	 * @param  Container $container
	 * @throws ObjectBuildingException
	 * @throws Exception
	 */
	private function injectFields(ReflectionClass $class, $object, array $fields, Container $container)
	{
		foreach ($fields as $field => $value)
		{
			if (array_key_exists($field, $object))
			{
				$object->$field = $this->resolveValue($value, $container);
			}
			elseif ($this->is53 && $class->hasProperty($field))
			{
				$property = $class->getProperty($field);
				$property->setAccessible(true);
				$property->setValue($object, $this->resolveValue($value, $container));
			}
			else
			{
				throw new ObjectBuildingException(sprintf(
					'Can not inject value of [%s::$%s] field.', $class->getName(), $field
				));
			}
		}
	}
	
	/**
	 * @param  object $object
	 * @param  array<MethodCall> $methods
	 * @param  Container $container
	 * @throws ObjectBuildingException
	 * @throws Exception
	 */
	private function injectMethods($object, array $methodCalls, Container $container)
	{
		/* @var $methodCall MethodCall */
		foreach ($methodCalls as $methodCall)
		{
			$callback = array($object, $methodCall->getMethodName());
			
			if (!is_callable($callback))
			{
				throw new ObjectBuildingException(sprintf(
					'[%s::%s()] method is not callable.', get_class($object), $methodCall->getMethodName()
				));
			}
			
			call_user_func_array($callback, $this->resolveValues($methodCall->getArguments(), $container));
		}
	}
	
	/**
	 * @param  array $values
	 * @param  Container $container
	 * @return array
	 */
	private function resolveValues(array $values, Container $container)
	{
		foreach ($values as &$value)
		{
			$value = $this->resolveValue($value, $container);
		}
		
		return $values;
	}
	
	/**
	 * @param  mixed $value
	 * @param  Container $container
	 * @return mixed
	 */
	private function resolveValue($value, Container $container)
	{
		if ($value instanceof ContainerAware)
		{
			$value->setContainer($container);
		}
		
		if (!$value instanceof ValueHolder)
		{
			return $value;
		}
		
		return $value->getValue($container);
	}
}